哈夫曼树求非叶子节点个数

有这样一个面试题:已知哈夫曼树有121个叶子节点,求非叶子节点的个数

没接触过相关概念或者忘了的,我稍微用白话解释下:

哈夫曼树:将N个数据结构当叶子节点,以其某个域的值V作为权重,把这些叶子节点组合成一棵树(N个节点都要存在且仅作为叶子节点),使得所有叶子节点到树根的加权路径和最小,即距离D*V的总和最小
树的度:树中某个节点最多有几个

举例来说,如果给定列表[1,2,3,4,5],组成一棵度为2的哈夫曼树,有如下步骤:

1、从中选择2个最小的构成一棵树,并计算构成后的树的权,1 + 2 = 3
2、将新构成的树重新加入列表得到:[4,5,3(1,2)]
3、如果新的列表大于2(度),跳转到步骤1,否则剩余节点全部加入一个根节点,完成构建

有了这些基本概念后,如何求解呢,我们先问下GPT看看

GPT-4给出的公式和推导过程如下:

首先,假设一棵度为m的哈夫曼树有n个叶子节点,x个非叶子节点,那么它的总节点数为n+x。
其次,观察这棵树的结构,可以发现除了根节点外,每个非叶子节点都有m个子节点,而每个叶子节点都没有子节点。所以,这棵树的总分支数为xm+0n,也就是xm。
再次,由树的基本性质可知,一棵有k个节点的树,它的分支数等于k-1。
所以,这棵树的总分支数也可以表示为n+x-1。 最后,将上面两个等式联立,得到xm=n+x-1,解得x=⌈(n-1)/ (m-1)⌉。

这个回答有启发意义,但仍然没有解释清楚,至少有两个疑问:

1)每个非叶子节点都有m个子节点吗?2)最后为何要向上取整?

第一个问题的答案显然是否定的,比如叶子节点为4、度为3的哈夫曼树,一定有一个非叶子节点有两个子节点而不是3个;而第二个问题答案是:向上取整就是解决上述例外的。

具体的,我们易得如下两个断言:

1)如果有非叶子节点不满,那么仅有一个不满的非叶子节点;因为同级别合并不改变结果,而向上合并可以减少层级从而减少加权路径总和。

2)不满的非叶子节点的子节点数量只会在[2,m-1]这个闭区间之内,没有只有一个子节点的非叶子节点;因为一个子节点的非叶子节点完全可以被他的子节点直接替换,还能减少一个层级,进而减少加权路径和。

于是,补齐这个不满的节点可得的准确的表达式及其推导形式:

x * m = n + y + x - 1
x = ((n-1)+y)/(m-1)

根据前述两个断言,补齐的y个节点,数量在[0,m-2]闭区间内,可以不需要补齐,但绝对不会补齐m-1个(断言二),但注意y不等于n%m,以叶子节点5、度3的哈夫曼树为例,不需要补齐(第一层3个节点,两个叶子节点,第二层三个叶子节点),而不是需要补齐2个。而补齐的最终目的就是为了让每个非叶子节点可以有m个子节点,所以对(n-1)/(m-1)向上取整即可以达到目的,求出最少满足条件的x。

还有一种利用构建规律来计算的方法,比如每一轮合并后都会从n个节点内先扣除m个节点再加入一个非叶子节点,那么可以用(n-1)/(m-1)再向上取整来计算。这种方式得出的公式一致,但是很难解释n-1以及向上取整。而且,当度大于等于3时,哈夫曼树的构建已经不能直接找M个最小节点合并了,需要加入一些占位节点补齐,例如[1,1,1,1,1,1]这个数列,如果按最小M合并方法,将得到[6,3(1,1,1),3(1,1,1)]这棵非常对称的树,但他的加权路径和12不是最小的,而加入一个占位节点0,得到[0,1,1,1,1,1,1],再利用最小M个节点合并法,将得到[6,1,2(0,1,1),3(1,1,1)],他的加权路径和为11,比前面的更小。

qemu线程池:一个semaphore的使用范例

qemu里面有个服务于aio的线程池:

struct ThreadPool {
    AioContext *ctx;
    QEMUBH *completion_bh;
    QemuMutex lock;
    QemuCond check_cancel;
    QemuCond worker_stopped;
    QemuSemaphore sem;
    int max_threads;
    QEMUBH *new_thread_bh;

    /* The following variables are only accessed from one AioContext. */
    QLIST_HEAD(, ThreadPoolElement) head;

    /* The following variables are protected by lock.  */
    QTAILQ_HEAD(, ThreadPoolElement) request_list;
    int cur_threads;
    int idle_threads;
    int new_threads;     /* backlog of threads we need to create */
    int pending_threads; /* threads created but not running yet */
    int pending_cancellations; /* whether we need a cond_broadcast */
    bool stopping;
};

qemu在thread_pool_init_one建池子的时候注册了一个延迟执行的work:
pool->new_thread_bh = aio_bh_new(ctx, spawn_thread_bh_fn, pool);

这个work将在spawn_thread里面被安排调度

static void spawn_thread(ThreadPool *pool)
{
    pool->cur_threads++;
    pool->new_threads++; //这里是为了记录下spawn thread被调用的次数,为后面安排的创建线程的后半部提供数据,因为不是每次调用进来都会安排后半部工作的
    /* If there are threads being created, they will spawn new workers, so
     * we don't spend time creating many threads in a loop holding a mutex or
     * starving the current vcpu.
     *
     * If there are no idle threads, ask the main thread to create one, so we
     * inherit the correct affinity instead of the vcpu affinity.
     */
    if (!pool->pending_threads) { //如果前面已经有安排,就不再重复安排
        qemu_bh_schedule(pool->new_thread_bh);
    }

 

当work被调度到的时候,执行spawn_thread_bh_fn

static void spawn_thread_bh_fn(void *opaque)
{
    ThreadPool *pool = opaque;

    qemu_mutex_lock(&pool->lock);
    do_spawn_thread(pool);
    qemu_mutex_unlock(&pool->lock);
}
static void do_spawn_thread(ThreadPool *pool)
{
    QemuThread t;

    /* Runs with lock taken.  */
    if (!pool->new_threads) {
        return;
    }

    pool->new_threads--;
    pool->pending_threads++; //避免重复安排

    qemu_thread_create(&t, "worker", worker_thread, pool, QEMU_THREAD_DETACHED);
}

 

这里采用了递归的方式来一次性创建多个worker,新创建的worker线程会递归调动do_spawn_thread来创建下一个worker,直到发现new_threads为0

这里有个问题,如果qemu_thread_create创建新线程失败,那么就会导致后面新的线程永远无法创建,因为pending_threads不会被减扣为0,前面spawn_thread就不会再安排新的下半部工作来创建线程了。

下面是aio派发任务的函数thread_pool_submit_aio,可以看到,池子中的线程数是动态增加的,如果有空闲的线程或者线程数已达上限是不会创建新的线程的,并且采用了信号量通知的方法来减少线程轮询开销,有任务的时候才放开一个额度,而不是让线程一直尝试拿后面的mutex锁再去看下request list是不是空。

qemu_mutex_lock(&pool->lock);
   if (pool->idle_threads == 0 && pool->cur_threads < pool->max_threads) {
       spawn_thread(pool);
   }
   QTAILQ_INSERT_TAIL(&pool->request_list, req, reqs);
   qemu_mutex_unlock(&pool->lock);
   qemu_sem_post(&pool->sem); //这里会增加sem的计数,让一个idle的worker获得执行机会
   return &req->common;

 

worker创建的时候以及worker在func执行完后会再次尝试获取pool->sem(等待sem>0),等待新的任务:

pool->idle_threads++;
qemu_mutex_unlock(&pool->lock);
ret = qemu_sem_timedwait(&pool->sem, 10000);//减扣sem计数
qemu_mutex_lock(&pool->lock);
pool->idle_threads--;

池子资源释放的时候,会标记pool->stopping并给所有worker一个最后的任务(通过sem_post):释放自己:

thread_pool_free:

/* Stop new threads from spawning */
qemu_bh_delete(pool->new_thread_bh);
pool->cur_threads -= pool->new_threads;
pool->new_threads = 0;

/* Wait for worker threads to terminate */
pool->stopping = true;
while (pool->cur_threads > 0) {
qemu_sem_post(&pool->sem);
qemu_cond_wait(&pool->worker_stopped, &pool->lock);
}

KVM shared MSRs

有些msr寄存器在用户态才有可能访问,内核态不会访问,那么我们vmexit的时候是不需要切换到host值的,而且vmcs里面没有保存相应的寄存器值,只在vcpu需要返回到用户态的时候才把这些msr的值保存到shared_msrs里面,同时切换到host之前保存的值。avi大神的patch解释了这种可以不用在内核态切换的寄存器的优化思路:https://lore.kernel.org/patchwork/cover/170941/

VMCS本身只对很少的一些MSR寄存器进行切换,所以原来大部分MSR的切换是依赖软件进行的,软件切换的开销很大,上述patch则是对此的优化。

这些寄存器一定不会在内核态被访问吗?KVM模块能保证自己不去访问,但是如何保证在内核开抢占的情况下,其他内核代码不会访问这些寄存器呢?AMD的svm里面比较诚实的提出了这种担心,侧面佐证了这确实是个隐患,需要开发者心中有数:
svm_vcpu_load:

    /* This assumes that the kernel never uses MSR_TSC_AUX */
    if (static_cpu_has(X86_FEATURE_RDTSCP))
        wrmsrl(MSR_TSC_AUX, svm->tsc_aux);
shared_msrs里面host值来自于vcpu_enter_guest调用kvm_x86_ops->prepare_guest_switch(vcpu)的时候保存的值。
vcpu返回用户态切换的过程是调用kvm_on_user_return函数,而这个函数也就是在kvm_x86_ops->prepare_guest_switch(vcpu)的时候调用kvm_set_shared_msr配置的,后者调用了内核函数:user_return_notifier_register(&smsr->urn); 这个函数是内核提供的返回用户态时刻的通知链接口,kvm用它来给vcpu返回挂了个钩子,如此实现了vcpu ioctl系统调用返回qemu时将shared_msrs切换回host之前保存的值。
kvm_on_user_return就是注册到内核的vcpu返回用户态时执行的逻辑,在切换shared_msrs的同时也把自己从通知链上注销了,因为一方面不需要别的进程在退出的时候也执行这个函数另一方面在vcpu enter guest的时候还会再注册的。

MSR从HOST切换到GUEST

该过程发生在vmentry时,最后调用链如下:
vcpu_enter_guest
    kvm_x86_ops->prepare_guest_switch(vcpu) ==> vmx_save_host_state
        kvm_set_shared_msr(vmx->guest_msrs[i].index, vmx->guest_msrs[i].data, vmx->guest_msrs[i].mask)
从host切到guest,guest的msr值从vmx->guest_msrs[i].data这里来
vmx->guest_msrs在vmx_get_msr时被读取,vmx_set_msr时被更改,vmx_get/set_msr一般在handle_rdmsr/wrmsr以及热迁移前后做save load msrs时被qemu触发调用。vmx_set_msr里面同样会调用kvm_set_shared_msr,这里其实更多是为了试一下是否可以设置成功,如果设置不成功那么要让vmx->guest_msrs里面相应的msr值保持原来的设置而不去更新,因为可能新设置的是个非法的值。实际上vmx_set_msr只需要更新guest_msrs就可以了,因为vmentry的时候还是会从guest_msrs里面读取再kvm_set_shared_msr。

MSR从GUEST切换到HOST

vmexit时并不会切share_msrs涉及到的msr,而是发生在vcpu要返回user mode之际,内核调用前面注册的通知函数,调用链如下:
prepare_exit_to_usermode
    exit_to_usermode_loop
        fire_user_return_notifiers
            urn->on_user_return(urn) ==> kvm_on_user_return 找到对应的shared_msrs并恢复到vmentry前的host值
static void kvm_on_user_return(struct user_return_notifier *urn)
{
    unsigned slot;
    struct kvm_shared_msrs *locals
        = container_of(urn, struct kvm_shared_msrs, urn);
    struct kvm_shared_msr_values *values;
    unsigned long flags;

    /*
     * Disabling irqs at this point since the following code could be
     * interrupted and executed through kvm_arch_hardware_disable()
     */
    local_irq_save(flags);
    if (locals->registered) {
        locals->registered = false;
        user_return_notifier_unregister(urn);
    }
    local_irq_restore(flags);
    for (slot = 0; slot < shared_msrs_global.nr; ++slot) {
        values = &locals->values[slot];
        if (values->host != values->curr) {
            wrmsrl(shared_msrs_global.msrs[slot], values->host);
            values->curr = values->host;
        }
    }
}

 

尝试讲清楚编辑距离求解

这篇文章是尝试给像作者一样的算法爱好者(业余的委婉说法)解释一下编辑距离求解方法的思考过程,其实也是为了强迫自己真正的吃透编辑距离求解的方法论,以期能达到举一反三的效果。根据以上受众定位,我首先还是引入一段对编辑距离的解释,然后再从递归到递推(动态规划)讲讲对这个问题求解本身的正向思考过程,而不是拿到一个解法来说明它的正确性。

编辑距离的定义是用增删改三种操作将一个字符串演变为一个目标字符串所需的操作次数。比如将horse转化为ros,可以是如下步骤:
1)删除h变成orse; 2)删除o变成rse; 3)增加o变成rose; 4)删除e变成ros
也可以是如下步骤:
1)改h为r变成rorse; 2)删r变成rose; 3)删e变成ros
显然第二种策略要优于第一种,但是这种演变策略的排列组合随着源串a和目的串b的长度增长会几何级数的增长。我们需要用程序来求解。

这是一个典型的考动态规划的编程题,很多文章都会给出一个递推公式,但是这个递推公式为何正确,得到这个公式的思考过程是啥,为什么这么思考?回答了这几个问题才算理解了编辑距离的精髓,而且才能有举一反三的可能性。看了很多解释的文章,感觉都是从结果来理解和解释递推过程反证过程的有效性,隔靴搔痒,没有讲到这个公式是怎么得来的,怎么一下子就能得出这么精妙的计算方法,感觉就像现在解释机器学习模型的原理那样,训练出来一个有效的神经网络结构,然后再试图解释每一层是在干嘛,然而并不是一开始设计的时候就知道会是这样。但编辑距离这个算法是完全由人想出来的,必然是有设计者的心路历程可循。下面讲下我对这个问题的理解思路。

最容易想到的方法是递归,递归函数以目标字符串和源字符串为输入参数,代码如下:

def lev(a, b):
    if len(a) == 0 or len(b) == 0:
        return max(len(a),len(b))
    if a[0] == b[0]:
        return lev(a[1:],b[1:]) #首字符相等则跳过
    ADD = lev(a,b[1:]) # add b[0]到a的开始, 问题转化为求a,b[1:]的编辑距离,因为头部相同的字符不增加编辑距离
    DEL = lev(a[1:],b) # del a[0]
    REP = lev(a[1:],b[1:]) # set a[0] = b[0], 问题转为求a[1:],b[1:],原因同计算a的解释
    return 1 + min(ADD,DEL,REP)

 

Continue reading “尝试讲清楚编辑距离求解”

vfio 直通设备的 memory region 初始化

vfio特别用一个数据结构来管理设备的memory region
typedef struct VFIORegion {
    struct VFIODevice *vbasedev;
    off_t fd_offset; /* offset of region within device fd */
    MemoryRegion *mem; /* slow, read/write access */
    size_t size;
    uint32_t flags; /* VFIO region flags (rd/wr/mmap) */
    uint32_t nr_mmaps;
    VFIOMmap *mmaps;
    uint8_t nr; /* cache the region number for debug */
} VFIORegion;

 

注意到它还有个域是VFIOMmap结构的指针:
typedef struct VFIOMmap {
    MemoryRegion mem;
    void *mmap;
    off_t offset;
    size_t size;
} VFIOMmap;

 

感觉是不是有冗余,那么外层结构里面的mem是不是指向内层中的mem呢?
上面的mem还有个奇怪的注释:slow,难道还有一种快一点的MR?那是不是指这里是IO的MR,另外还有个RAM的mr?

Continue reading “vfio 直通设备的 memory region 初始化”

AI来了。云计算凉了?

笔者身处云计算行业,如今的IT界言必称AI给我带来了巨大的精神压力,无时不在思考AI和云计算的关系,云计算在AI大潮下是不是已经明日黄花了,是不是要转行做AI?所谓人类一思考上帝就发笑,我觉得上帝没这么肤浅,毕竟咱也是上帝的AI产品不是~,有空还是要自我迭代一下,继续自觉优化下肉神经网络参数的

Continue reading “AI来了。云计算凉了?”

IOMMU group and ACS cap

VFIO做VM的设备直通过程中,需要把直通设备所在iommu group里面所有的设备都unbind掉,这是为啥呢,iommu group又是啥,木有遇到该问题的小伙伴你们肯定年轻而富有:)你们的设备都是有ACS的呢,这又是啥,咱先来看看官方文档吧:

Continue reading “IOMMU group and ACS cap”

libvirt 向qemu传文件描述符

libvirt创建的qemu进程里面有一些fd的参数,这些文件是libvirt帮qemu打开的一些设备文件句柄等,比如:
qemu … -netdev tap,fd=24,id=hostnet1,vhost=on,vhostfd=25
因为需要libvirt帮忙先配置好后端以及处于安全考虑;但是qemu起来就是另外一个进程,给个fd号就能直接用了吗,显然不是,下面从代码角度分析下

Continue reading “libvirt 向qemu传文件描述符”

sriov vf get iommu group kernel code trace

VF device driver call:
pci_enable_sriov -> sriov_enable -> pci_iov_add_virtfn -> pci_device_add -> device_add ->
    blocking_notifier_call_chain(&dev->bus->p->bus_notifier,
                                             BUS_NOTIFY_ADD_DEVICE, dev);
pci bus register a iommu notifier will be called when device_add start to notify:
static int __init pci_iommu_init(void)
{
        if (iommu_detected)
                intel_iommu_init();

        return 0;
}

/* Must execute after PCI subsystem */
fs_initcall(pci_iommu_init);

intel_iommu_init -> iommu_bus_init -> “nb->notifier_call = iommu_bus_notifier;”

        if (action == BUS_NOTIFY_ADD_DEVICE) {
                if (ops->add_device)
                        return ops->add_device(dev);
ops->add_device:
static struct iommu_ops intel_iommu_ops = {
        .capable        = intel_iommu_capable,
        .domain_alloc   = intel_iommu_domain_alloc,
        .domain_free    = intel_iommu_domain_free,
        .attach_dev     = intel_iommu_attach_device,
        .detach_dev     = intel_iommu_detach_device,
        .map            = intel_iommu_map,
        .unmap          = intel_iommu_unmap,
        .map_sg         = default_iommu_map_sg,
        .iova_to_phys   = intel_iommu_iova_to_phys,
        .add_device     =intel_iommu_add_device,
        .remove_device  = intel_iommu_remove_device,
        .pgsize_bitmap  = INTEL_IOMMU_PGSIZES,
};

intel_iommu_add_device:

static int intel_iommu_add_device(struct device *dev)
{
        struct intel_iommu *iommu;
        struct iommu_group *group;
        u8 bus, devfn;

        iommu = device_to_iommu(dev, &bus, &devfn);
        if (!iommu)
                return -ENODEV;

        iommu_device_link(iommu->iommu_dev, dev);

        group = iommu_group_get_for_dev(dev);

        if (IS_ERR(group))
                return PTR_ERR(group);

        iommu_group_put(group);
        return 0;
}
iommu_group_get_for_dev:
this will find or create an iommu group for the VF device